
package org.apache.solr.core;

import java.io.FileFilter;
import java.io.IOException;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPathConstants;
import org.apache.lucene.index.IndexDeletionPolicy;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.util.Version;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
import org.apache.solr.handler.component.SearchComponent;
import org.apache.solr.request.SolrRequestHandler;
import org.apache.solr.response.QueryResponseWriter;
import org.apache.solr.response.transform.TransformerFactory;
import org.apache.solr.search.CacheConfig;
import org.apache.solr.search.FastLRUCache;
import org.apache.solr.search.QParserPlugin;
import org.apache.solr.search.ValueSourceParser;
import org.apache.solr.spelling.QueryConverter;
import org.apache.solr.update.SolrIndexConfig;
import org.apache.solr.update.UpdateLog;
import org.apache.solr.update.processor.UpdateRequestProcessorChain;
import org.apache.solr.util.DOMUtil;
import org.apache.solr.util.RegexFileFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * Provides a static reference to a Config object modeling the main
 * configuration data for a a Solr instance -- typically found in
 * "solrconfig.xml".
 */
public class SolrConfig extends Config {

    public static final Logger log = LoggerFactory.getLogger(SolrConfig.class);

    public static final String DEFAULT_CONF_FILE = "solrconfig.xml";

    /**
     * Creates a default instance from the solrconfig.xml.
     */
    public SolrConfig() throws ParserConfigurationException, IOException, SAXException {
        this((SolrResourceLoader)null, DEFAULT_CONF_FILE, null);
    }

    /**
     * Creates a configuration instance from a configuration name. A default
     * resource loader will be created (
     *
     * @see SolrResourceLoader)
     * @param name the configuration name used by the loader
     */
    public SolrConfig(String name) throws ParserConfigurationException, IOException, SAXException {
        this((SolrResourceLoader)null, name, null);
    }

    /**
     * Creates a configuration instance from a configuration name and stream. A
     * default resource loader will be created (
     *
     * @see SolrResourceLoader). If the stream is null, the resource loader will
     * open the configuration stream. If the stream is not null, no attempt to
     * load the resource will occur (the name is not used).
     * @param name the configuration name
     * @param is the configuration stream
     */
    public SolrConfig(String name, InputSource is) throws ParserConfigurationException, IOException, SAXException {
        this((SolrResourceLoader)null, name, is);
    }

    /**
     * Creates a configuration instance from an instance directory,
     * configuration name and stream.
     *
     * @param instanceDir the directory used to create the resource loader
     * @param name the configuration name used by the loader if the stream is
     * null
     * @param is the configuration stream
     */
    public SolrConfig(String instanceDir, String name, InputSource is) throws ParserConfigurationException, IOException, SAXException {
        this(new SolrResourceLoader(instanceDir), name, is);
    }

    /**
     * Creates a configuration instance from a resource loader, a configuration
     * name and a stream. If the stream is null, the resource loader will open
     * the configuration stream. If the stream is not null, no attempt to load
     * the resource will occur (the name is not used).
     *
     * @param loader the resource loader
     * @param name the configuration name
     * @param is the configuration stream
     */
    public SolrConfig(SolrResourceLoader loader, String name, InputSource is) throws ParserConfigurationException, IOException, SAXException {

        super(loader, name, is, "/config/");
        initLibs();

        luceneMatchVersion = getLuceneVersion("luceneMatchVersion");
        String indexConfigPrefix;

        // Old indexDefaults and mainIndex sections are deprecated and fails fast for luceneMatchVersion=>LUCENE_40.
        // For older solrconfig.xml's we allow the old sections, but never mixed with the new <indexConfig>
        boolean hasDeprecatedIndexConfig = get("indexDefaults/text()", null) != null || get("mainIndex/text()", null) != null;
        boolean hasNewIndexConfig = get("indexConfig/text()", null) != null;
        if(hasDeprecatedIndexConfig) {
            if(luceneMatchVersion.onOrAfter(Version.LUCENE_40)) {
                throw new SolrException(ErrorCode.FORBIDDEN, "<indexDefaults> and <mainIndex> configuration sections are discontinued. Use <indexConfig> instead.");
            }
            else {
                // Still allow the old sections for older LuceneMatchVersion's
                if(hasNewIndexConfig) {
                    throw new SolrException(ErrorCode.FORBIDDEN, "Cannot specify both <indexDefaults>, <mainIndex> and <indexConfig> at the same time. Please use <indexConfig> only.");
                }
                log.warn("<indexDefaults> and <mainIndex> configuration sections are deprecated and will fail for luceneMatchVersion=LUCENE_40 and later. Please use <indexConfig> instead.");
                defaultIndexConfig = new SolrIndexConfig(this, "indexDefaults", null);
                mainIndexConfig = new SolrIndexConfig(this, "mainIndex", defaultIndexConfig);
                indexConfigPrefix = "mainIndex";
            }
        }
        else {
            defaultIndexConfig = mainIndexConfig = null;
            indexConfigPrefix = "indexConfig";
        }
        // Parse indexConfig section, using mainIndex as backup in case old config is used
        indexConfig = new SolrIndexConfig(this, "indexConfig", mainIndexConfig);

        reopenReaders = getBool(indexConfigPrefix + "/reopenReaders", true);

        booleanQueryMaxClauseCount = getInt("query/maxBooleanClauses", BooleanQuery.getMaxClauseCount());
        log.info("Using Lucene MatchVersion: " + luceneMatchVersion);

        // Warn about deprecated / discontinued parameters
        // boolToFilterOptimizer has had no effect since 3.1 
        if(get("query/boolTofilterOptimizer", null) != null) {
            log.warn("solrconfig.xml: <boolTofilterOptimizer> is currently not implemented and has no effect.");
        }
        if(get("query/HashDocSet", null) != null) {
            log.warn("solrconfig.xml: <HashDocSet> is deprecated and no longer recommended used.");
        }

// TODO: Old code - in case somebody wants to re-enable. Also see SolrIndexSearcher#search()
//    filtOptEnabled = getBool("query/boolTofilterOptimizer/@enabled", false);
//    filtOptCacheSize = getInt("query/boolTofilterOptimizer/@cacheSize",32);
//    filtOptThreshold = getFloat("query/boolTofilterOptimizer/@threshold",.05f);

        useFilterForSortedQuery = getBool("query/useFilterForSortedQuery", false);
        queryResultWindowSize = Math.max(1, getInt("query/queryResultWindowSize", 1));
        queryResultMaxDocsCached = getInt("query/queryResultMaxDocsCached", Integer.MAX_VALUE);
        enableLazyFieldLoading = getBool("query/enableLazyFieldLoading", false);


        filterCacheConfig = CacheConfig.getConfig(this, "query/filterCache");
        queryResultCacheConfig = CacheConfig.getConfig(this, "query/queryResultCache");
        documentCacheConfig = CacheConfig.getConfig(this, "query/documentCache");
        CacheConfig conf = CacheConfig.getConfig(this, "query/fieldValueCache");
        if(conf == null) {
            Map<String, String> args = new HashMap<>();
            args.put("name", "fieldValueCache");
            args.put("size", "10000");
            args.put("initialSize", "10");
            args.put("showItems", "-1");
            conf = new CacheConfig(FastLRUCache.class, args, null);
        }
        fieldValueCacheConfig = conf;
        unlockOnStartup = getBool(indexConfigPrefix + "/unlockOnStartup", false);
        useColdSearcher = getBool("query/useColdSearcher", false);
        dataDir = get("dataDir", null);
        if(dataDir != null && dataDir.length() == 0) {
            dataDir = null;
        }

        userCacheConfigs = CacheConfig.getMultipleConfigs(this, "query/cache");

        org.apache.solr.search.SolrIndexSearcher.initRegenerators(this);

        hashSetInverseLoadFactor = 1.0f / getFloat("//HashDocSet/@loadFactor", 0.75f);
        hashDocSetMaxSize = getInt("//HashDocSet/@maxSize", 3000);

        httpCachingConfig = new HttpCachingConfig(this);

        Node jmx = getNode("jmx", false);
        if(jmx != null) {
            jmxConfig = new JmxConfiguration(true,
                    get("jmx/@agentId", null),
                    get("jmx/@serviceUrl", null),
                    get("jmx/@rootName", null));

        }
        else {
            jmxConfig = new JmxConfiguration(false, null, null, null);
        }
        maxWarmingSearchers = getInt("query/maxWarmingSearchers", Integer.MAX_VALUE);

        loadPluginInfo(SolrRequestHandler.class, "requestHandler", true, true);
        loadPluginInfo(QParserPlugin.class, "queryParser", true, true);
        loadPluginInfo(QueryResponseWriter.class, "queryResponseWriter", true, true);
        loadPluginInfo(ValueSourceParser.class, "valueSourceParser", true, true);
        loadPluginInfo(TransformerFactory.class, "transformer", true, true);
        loadPluginInfo(SearchComponent.class, "searchComponent", true, true);
        loadPluginInfo(QueryConverter.class, "queryConverter", true, true);

        // this is hackish, since it picks up all SolrEventListeners,
        // regardless of when/how/why thye are used (or even if they are 
        // declared outside of the appropriate context) but there's no nice 
        // way arround that in the PluginInfo framework
        loadPluginInfo(SolrEventListener.class, "//listener", false, true);

        loadPluginInfo(DirectoryFactory.class, "directoryFactory", false, true);
        loadPluginInfo(IndexDeletionPolicy.class, indexConfigPrefix + "/deletionPolicy", false, true);
        loadPluginInfo(CodecFactory.class, "codecFactory", false, false);
        loadPluginInfo(IndexReaderFactory.class, "indexReaderFactory", false, true);
        loadPluginInfo(UpdateRequestProcessorChain.class, "updateRequestProcessorChain", false, false);
        loadPluginInfo(UpdateLog.class, "updateHandler/updateLog", false, false);

        updateHandlerInfo = loadUpdatehandlerInfo();

        Config.log.info("Loaded SolrConfig: " + name);
    }

    protected UpdateHandlerInfo loadUpdatehandlerInfo() {
        return new UpdateHandlerInfo(get("updateHandler/@class", null),
                getInt("updateHandler/autoCommit/maxDocs", -1),
                getInt("updateHandler/autoCommit/maxTime", -1),
                getBool("updateHandler/autoCommit/openSearcher", true),
                getInt("updateHandler/commitIntervalLowerBound", -1),
                getInt("updateHandler/autoSoftCommit/maxDocs", -1),
                getInt("updateHandler/autoSoftCommit/maxTime", -1));
    }

    private void loadPluginInfo(Class clazz, String tag, boolean requireName, boolean requireClass) {
        List<PluginInfo> result = readPluginInfos(tag, requireName, requireClass);
        if(!result.isEmpty()) {
            pluginStore.put(clazz.getName(), result);
        }
    }

    public List<PluginInfo> readPluginInfos(String tag, boolean requireName, boolean requireClass) {
        ArrayList<PluginInfo> result = new ArrayList<>();
        NodeList nodes = (NodeList)evaluate(tag, XPathConstants.NODESET);
        for(int i = 0; i < nodes.getLength(); i++) {
            PluginInfo pluginInfo = new PluginInfo(nodes.item(i), "[solrconfig.xml] " + tag, requireName, requireClass);
            if(pluginInfo.isEnabled()) {
                result.add(pluginInfo);
            }
        }
        return result;
    }

    /* The set of materialized parameters: */
    public final int booleanQueryMaxClauseCount;
    // SolrIndexSearcher - caches configurations
    public final CacheConfig filterCacheConfig;
    public final CacheConfig queryResultCacheConfig;
    public final CacheConfig documentCacheConfig;
    public final CacheConfig fieldValueCacheConfig;
    public final CacheConfig[] userCacheConfigs;
    // SolrIndexSearcher - more...
    public final boolean useFilterForSortedQuery;
    public final int queryResultWindowSize;
    public final int queryResultMaxDocsCached;
    public final boolean enableLazyFieldLoading;
    public final boolean reopenReaders;
    // DocSet
    public final float hashSetInverseLoadFactor;
    public final int hashDocSetMaxSize;
    // default & main index configurations, deprecated as of 3.6
    @Deprecated
    public final SolrIndexConfig defaultIndexConfig;
    @Deprecated
    public final SolrIndexConfig mainIndexConfig;
    // IndexConfig settings
    public final SolrIndexConfig indexConfig;
    protected UpdateHandlerInfo updateHandlerInfo;
    private Map<String, List<PluginInfo>> pluginStore = new LinkedHashMap<>();
    public final int maxWarmingSearchers;
    public final boolean unlockOnStartup;
    public final boolean useColdSearcher;
    public final Version luceneMatchVersion;
    protected String dataDir;
    //JMX configuration
    public final JmxConfiguration jmxConfig;
    private final HttpCachingConfig httpCachingConfig;

    public HttpCachingConfig getHttpCachingConfig() {
        return httpCachingConfig;
    }

    public static class JmxConfiguration {

        public boolean enabled = false;
        public String agentId;
        public String serviceUrl;
        public String rootName;

        public JmxConfiguration(boolean enabled, String agentId, String serviceUrl, String rootName) {
            this.enabled = enabled;
            this.agentId = agentId;
            this.serviceUrl = serviceUrl;
            this.rootName = rootName;

            if(agentId != null && serviceUrl != null) {
                throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Incorrect JMX Configuration in solrconfig.xml, "
                        + "both agentId and serviceUrl cannot be specified at the same time");
            }
        }
    }

    public static class HttpCachingConfig {

        /**
         * config xpath prefix for getting HTTP Caching options
         */
        private final static String CACHE_PRE = "requestDispatcher/httpCaching/";
        /**
         * For extracting Expires "ttl" from <cacheControl> config
         */
        private final static Pattern MAX_AGE = Pattern.compile("\\bmax-age=(\\d+)");

        public static enum LastModFrom {

            OPENTIME, DIRLASTMOD, BOGUS;

            /**
             * Input must not be null
             */
            public static LastModFrom parse(final String s) {
                try {
                    return valueOf(s.toUpperCase(Locale.ROOT));
                }
                catch(Exception e) {
                    log.warn("Unrecognized value for lastModFrom: " + s, e);
                    return BOGUS;
                }
            }
        }
        private final boolean never304;
        private final String etagSeed;
        private final String cacheControlHeader;
        private final Long maxAge;
        private final LastModFrom lastModFrom;

        private HttpCachingConfig(SolrConfig conf) {

            never304 = conf.getBool(CACHE_PRE + "@never304", false);

            etagSeed = conf.get(CACHE_PRE + "@etagSeed", "Solr");


            lastModFrom = LastModFrom.parse(conf.get(CACHE_PRE + "@lastModFrom", "openTime"));

            cacheControlHeader = conf.get(CACHE_PRE + "cacheControl", null);

            Long tmp = null; // maxAge
            if(null != cacheControlHeader) {
                try {
                    final Matcher ttlMatcher = MAX_AGE.matcher(cacheControlHeader);
                    final String ttlStr = ttlMatcher.find() ? ttlMatcher.group(1) : null;
                    tmp = (null != ttlStr && !"".equals(ttlStr)) ? Long.valueOf(ttlStr) : null;
                }
                catch(Exception e) {
                    log.warn("Ignoring exception while attempting to " + "extract max-age from cacheControl config: " + cacheControlHeader, e);
                }
            }
            maxAge = tmp;

        }

        public boolean isNever304() {
            return never304;
        }

        public String getEtagSeed() {
            return etagSeed;
        }

        /**
         * null if no Cache-Control header
         */
        public String getCacheControlHeader() {
            return cacheControlHeader;
        }

        /**
         * null if no max age limitation
         */
        public Long getMaxAge() {
            return maxAge;
        }

        public LastModFrom getLastModFrom() {
            return lastModFrom;
        }
    }

    public static class UpdateHandlerInfo {

        public final String className;
        public final int autoCommmitMaxDocs, autoCommmitMaxTime, commitIntervalLowerBound, autoSoftCommmitMaxDocs, autoSoftCommmitMaxTime;
        public final boolean openSearcher;  // is opening a new searcher part of hard autocommit?

        /**
         * @param autoCommmitMaxDocs set -1 as default
         * @param autoCommmitMaxTime set -1 as default
         * @param commitIntervalLowerBound set -1 as default
         */
        public UpdateHandlerInfo(String className, int autoCommmitMaxDocs, int autoCommmitMaxTime, boolean openSearcher, int commitIntervalLowerBound,
                int autoSoftCommmitMaxDocs, int autoSoftCommmitMaxTime) {
            this.className = className;
            this.autoCommmitMaxDocs = autoCommmitMaxDocs;
            this.autoCommmitMaxTime = autoCommmitMaxTime;
            this.openSearcher = openSearcher;
            this.commitIntervalLowerBound = commitIntervalLowerBound;

            this.autoSoftCommmitMaxDocs = autoSoftCommmitMaxDocs;
            this.autoSoftCommmitMaxTime = autoSoftCommmitMaxTime;
        }
    }

//  public Map<String, List<PluginInfo>> getUpdateProcessorChainInfo() { return updateProcessorChainInfo; }
    public UpdateHandlerInfo getUpdateHandlerInfo() {
        return updateHandlerInfo;
    }

    public String getDataDir() {
        return dataDir;
    }

    /**
     * SolrConfig keeps a repository of plugins by the type. The known
     * interfaces are the types.
     *
     * @param type The key is FQN of the plugin class there are a few known
     * types : SolrFormatter, SolrFragmenter SolrRequestHandler,QParserPlugin,
     * QueryResponseWriter,ValueSourceParser, SearchComponent, QueryConverter,
     * SolrEventListener, DirectoryFactory, IndexDeletionPolicy,
     * IndexReaderFactory, {@link TransformerFactory}
     */
    public List<PluginInfo> getPluginInfos(String type) {

        List<PluginInfo> result = pluginStore.get(type);
        return result == null ? Collections.<PluginInfo>emptyList() : result;
    }

    public PluginInfo getPluginInfo(String type) {

        List<PluginInfo> result = pluginStore.get(type);
        return result == null || result.isEmpty() ? null : result.get(0);
    }

    private void initLibs() {

        NodeList nodes = (NodeList)evaluate("lib", XPathConstants.NODESET);
        if(nodes == null || nodes.getLength() == 0) {
            return;
        }

        log.info("Adding specified lib dirs to ClassLoader");

        try {
            for(int i = 0; i < nodes.getLength(); i++) {
                Node node = nodes.item(i);

                String baseDir = DOMUtil.getAttr(node, "dir");
                String path = DOMUtil.getAttr(node, "path");
                if(null != baseDir) {
                    // :TODO: add support for a simpler 'glob' mutually eclusive of regex
                    String regex = DOMUtil.getAttr(node, "regex");
                    FileFilter filter = (null == regex) ? null : new RegexFileFilter(regex);
                    getResourceLoader().addToClassLoader(baseDir, filter);
                }
                else if(null != path) {
                    getResourceLoader().addToClassLoader(path);
                }
                else {
                    throw new RuntimeException("lib: missing mandatory attributes: 'dir' or 'path'");
                }
            }
        }
        finally {
            getResourceLoader().reloadLuceneSPI();
        }
    }
}
